package com.codingchili.logging;


import com.codingchili.logging.configuration.LogContext;
import com.codingchili.logging.controller.*;
import com.codingchili.logging.model.*;
import io.vertx.core.*;
import io.vertx.core.json.JsonObject;

import java.util.Set;

import com.codingchili.core.configuration.CoreStrings;
import com.codingchili.core.context.CoreContext;
import com.codingchili.core.context.TimerSource;
import com.codingchili.core.listener.CoreService;
import com.codingchili.core.listener.Receiver;
import com.codingchili.core.logging.Logger;
import com.codingchili.core.protocol.*;
import com.codingchili.core.storage.AsyncStorage;
import com.codingchili.core.storage.JsonStorable;
import com.codingchili.core.storage.exception.ValueMissingException;

import static com.codingchili.common.Strings.*;
import static com.codingchili.core.configuration.CoreStrings.ID_STATISTICS;
import static com.codingchili.core.context.FutureHelper.untyped;
import static com.codingchili.core.protocol.RoleMap.PUBLIC;

/**
 * This service deploys logging handlers for the servers, clients and
 * a statistics handler which serves clients data from the statistical model.
 * <p>
 * This service consumes logging events that is registered on the protocol object
 * which is then used to update the statistical model. The model is shared using
 * vert.x shared-data.
 */
@Roles(PUBLIC)
public class Service implements CoreService, Receiver<JsonObject> {
    public static final int MODEL_INTERVAL_MS = 800;
    public static final String SHARED_MAP = "statistics.map";
    public static final int PERSIST_INTERVAL_MS = 5000;
    private final Protocol<JsonObject> protocol = new Protocol<>(this);
    private AsyncStorage<JsonStorable> store;
    private StatisticModel model = new StatisticModel();
    private LogContext context;
    private Logger logger;

    @Override
    public void init(CoreContext core) {
        this.context = new LogContext(core);
        this.logger = context.logger(getClass());
    }

    /**
     * @return a list of events registered on the protocol, these are the
     * events that should be processed for statistics.
     */
    public static Set<String> processableEventNames() {
        return new Protocol<>(new Service()).available();
    }

    @Override
    public void start(Promise<Void> start) {
        CompositeFuture.all(
                        loadStatisticalModel(context),
                        context.handler(() -> new StatisticsHandler(context)),
                        context.handler(() -> new ServiceLogHandler(context)),
                        context.handler(() -> new ClientLogHandler(context))
                ).onFailure(e -> start.fail(e.getCause()))
                .onSuccess((v) -> {
                    context.bus().<JsonObject>localConsumer(ID_STATISTICS)
                            .handler(message -> this.handle(message.body()));
                }).onComplete(untyped(start));
    }

    @Api(route = LOG_ACCOUNT_REGISTERED)
    public void registered(JsonObject json) {
        model.registerAccount();
    }

    @Api(route = CLIENT_CHARACTER_CREATE)
    public void character_created(JsonObject json) {
        var create = json.mapTo(CharacterCreate.class);

        model.createCharacter();
        model.realm(create.getRealm())
                .characterCreated(create.getClassId());
    }

    @Api(route = LOG_PLAYER_LEAVE)
    public void leaderboard(JsonObject json) {
        model.getLeaderboard().add(json.mapTo(LeaderboardEntry.class));
    }

    private Future<Void> loadStatisticalModel(LogContext context) {
        var promise = Promise.<Void>promise();

        if (context.storageEnabled()) {
            context.storage(context.service().getStatistics()).onSuccess(store -> {
                this.store = store;
                store.get(StatisticModel.id(), model -> {
                    if (model.succeeded()) {
                        this.model = model.result().mapTo(StatisticModel.class);
                        promise.complete();
                    } else {
                        if (model.cause() instanceof ValueMissingException) {
                            // does not exist yet, start from scratch.
                            this.model = new StatisticModel();
                            promise.complete();
                        } else {
                            promise.fail(model.cause());
                        }
                    }
                });
            }).onFailure(promise::fail);
        } else {
            model = new StatisticModel();
            promise.complete();
        }
        return promise.future().compose(this::save);
    }

    private Future<Void> save(Void unused) {
        if (context.storageEnabled()) {
            // persist the statistical model to storage to keep data between restarts.
            context.periodic(TimerSource.of(PERSIST_INTERVAL_MS), v -> {
                store.put(model.toStorable(), (put) -> {
                    if (put.failed()) {
                        context.logger(getClass()).onError(put.cause());
                    }
                });
            });
        }
        // retrieve the async map used to share the statistical model between deployments.
        return context.vertx().sharedData().<String, StatisticModel>getLocalAsyncMap(SHARED_MAP)
                .onSuccess(map -> {
                    // periodically store the latest version of the statistical model in the shared map.
                    context.periodic(TimerSource.of(MODEL_INTERVAL_MS), v -> map.put(model.getId(), model)
                            .onFailure(logger::onError));
                }).map(m -> null); // drop type information.
    }

    public void handle(JsonObject message) {
        try {
            protocol.get(message.getString(CoreStrings.LOG_EVENT)).submit(message);
        } catch (Throwable e) {
            logger.onError(e);
        }
    }
}
